// Copyright 2024 - 2025 Crunchy Data Solutions, Inc.
//
// SPDX-License-Identifier: Apache-2.0

package collector

import (
	"context"
	_ "embed"
	"fmt"
	"maps"
	"math"
	"strings"
	"time"

	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/util/sets"
	"sigs.k8s.io/yaml"

	"github.com/crunchydata/postgres-operator/pkg/apis/postgres-operator.crunchydata.com/v1beta1"
)

// The contents of "logrotate.conf" as a string.
// See: https://pkg.go.dev/embed
//
//go:embed "logrotate.conf"
var logrotateConfigFormatString string

// ComponentID represents a component identifier within an OpenTelemetry
// Collector YAML configuration. Each value is a "type" followed by an optional
// slash-then-name: `type[/name]`
type ComponentID = string

// PipelineID represents a pipeline identifier within an OpenTelemetry Collector
// YAML configuration. Each value is a signal followed by an optional
// slash-then-name: `signal[/name]`
type PipelineID = string

// Config represents an OpenTelemetry Collector YAML configuration.
// See: https://opentelemetry.io/docs/collector/configuration
type Config struct {
	Exporters  map[ComponentID]any
	Extensions map[ComponentID]any
	Processors map[ComponentID]any
	Receivers  map[ComponentID]any

	Pipelines map[PipelineID]Pipeline
}

// Pipeline represents the YAML configuration of a flow of telemetry data
// through an OpenTelemetry Collector.
// See: https://opentelemetry.io/docs/collector/configuration#pipelines
type Pipeline struct {
	Extensions []ComponentID
	Exporters  []ComponentID
	Processors []ComponentID
	Receivers  []ComponentID
}

// LogrotateConfig represents the configurable pieces of a log rotate config
// that can vary based on the specific component whose logs are being rotated
type LogrotateConfig struct {
	LogFiles         []string
	PostrotateScript string
}

func (c *Config) ToYAML() (string, error) {
	const yamlGeneratedWarning = "" +
		"# Generated by postgres-operator. DO NOT EDIT.\n" +
		"# Your changes will not be saved.\n"

	extensions := sets.New[ComponentID]()
	pipelines := make(map[PipelineID]any, len(c.Pipelines))

	for id, p := range c.Pipelines {
		extensions.Insert(p.Extensions...)
		pipelines[id] = map[string]any{
			"exporters":  p.Exporters,
			"processors": p.Processors,
			"receivers":  p.Receivers,
		}
	}

	b, err := yaml.Marshal(map[string]any{
		"exporters":  c.Exporters,
		"extensions": c.Extensions,
		"processors": c.Processors,
		"receivers":  c.Receivers,
		"service": map[string]any{
			"extensions": sets.List(extensions), // Sorted
			"pipelines":  pipelines,
		},
	})
	return string(append([]byte(yamlGeneratedWarning), b...)), err
}

// NewConfig creates a base config for an OTel collector container
func NewConfig(spec *v1beta1.InstrumentationSpec) *Config {
	config := &Config{
		Exporters: map[ComponentID]any{
			// https://pkg.go.dev/go.opentelemetry.io/collector/exporter/debugexporter#section-readme
			DebugExporter: map[string]any{"verbosity": "detailed"},
		},
		Extensions: map[ComponentID]any{},
		Processors: map[ComponentID]any{
			// https://pkg.go.dev/go.opentelemetry.io/collector/processor/batchprocessor#section-readme
			OneSecondBatchProcessor: map[string]any{"timeout": "1s"},
			SubSecondBatchProcessor: map[string]any{"timeout": "200ms"},

			// https://pkg.go.dev/github.com/open-telemetry/opentelemetry-collector-contrib/processor/groupbyattrsprocessor#readme-compaction
			CompactingProcessor: map[string]any{},
		},
		Receivers: map[ComponentID]any{},
		Pipelines: map[PipelineID]Pipeline{},
	}

	// Configure a batch processor for logs according to the API spec.
	// Use API defaults for any unspecified fields.
	{
		var batches v1beta1.OpenTelemetryLogsBatchSpec
		if spec != nil && spec.Logs != nil && spec.Logs.Batches != nil {
			spec.Logs.Batches.DeepCopyInto(&batches)
		}
		batches.Default()

		// https://pkg.go.dev/go.opentelemetry.io/collector/processor/batchprocessor#section-readme
		processor := map[string]any{}
		if batches.MaxDelay != nil {
			processor["timeout"] = batches.MaxDelay.AsDuration().Duration.String()
		}
		if batches.MaxRecords != nil {
			processor["send_batch_max_size"] = *batches.MaxRecords
		}
		if batches.MinRecords != nil {
			processor["send_batch_size"] = *batches.MinRecords
		}
		config.Processors[LogsBatchProcessor] = processor
	}

	// Create a resource detection processor according to the API spec.
	// When nothing is specified, the processor does nothing.
	{
		// https://pkg.go.dev/github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor#section-readme
		processor := map[string]any{"override": false, "timeout": "30s"}

		if spec != nil && spec.Config != nil {
			names := make([]string, len(spec.Config.Detectors))
			for i, detector := range spec.Config.Detectors {
				names[i] = detector.Name

				if len(detector.Attributes) > 0 {
					attributes := make(map[string]any, len(detector.Attributes))
					for k, v := range detector.Attributes {
						attributes[k] = map[string]any{"enabled": v}
					}
					processor[detector.Name] = map[string]any{
						"resource_attributes": attributes,
					}
				}
			}
			processor["detectors"] = names
		} else {
			processor["detectors"] = []string{}
		}

		config.Processors[ResourceDetectionProcessor] = processor
	}

	// If there are exporters defined in the spec, add them to the config.
	if spec != nil && spec.Config != nil && spec.Config.Exporters != nil {
		maps.Copy(config.Exporters, spec.Config.Exporters)
	}

	return config
}

// AddLogrotateConfigs generates a logrotate configuration for each LogrotateConfig
// provided via the configs parameter and adds them to the provided configmap.
func AddLogrotateConfigs(ctx context.Context, spec *v1beta1.InstrumentationSpec,
	outInstanceConfigMap *corev1.ConfigMap, configs []LogrotateConfig,
) {
	if outInstanceConfigMap.Data == nil {
		outInstanceConfigMap.Data = make(map[string]string)
	}

	// If retentionPeriod is set in the spec, use that value; otherwise, we want
	// to use a reasonably short duration. Defaulting to 1 day.
	retentionPeriod := metav1.Duration{Duration: 24 * time.Hour}
	if spec != nil && spec.Logs != nil && spec.Logs.RetentionPeriod != nil {
		retentionPeriod = spec.Logs.RetentionPeriod.AsDuration()
	}

	logrotateConfig := ""
	for _, config := range configs {
		logrotateConfig += generateLogrotateConfig(config, retentionPeriod)
	}

	outInstanceConfigMap.Data["logrotate.conf"] = logrotateConfig
}

// generateLogrotateConfig generates a configuration string for logrotate based
// on the provided full log file path, retention period, and postrotate script
func generateLogrotateConfig(
	config LogrotateConfig, retentionPeriod metav1.Duration,
) string {
	number, interval := ParseDurationForLogrotate(retentionPeriod)

	return fmt.Sprintf(
		logrotateConfigFormatString,
		strings.Join(config.LogFiles, " "),
		number,
		interval,
		config.PostrotateScript,
	)
}

// ParseDurationForLogrotate takes a retention period and returns the rotate
// number and interval string that should be used in the logrotate config.
// If the retentionPeriod is less than 24 hours, the function will return the
// number of hours and "hourly"; otherwise, we will round up to the nearest day
// and return the day count and "daily"
func ParseDurationForLogrotate(retentionPeriod metav1.Duration) (int, string) {
	hours := math.Ceil(retentionPeriod.Hours())
	if hours < 24 {
		return int(hours), "hourly"
	}
	return int(math.Ceil(hours / 24)), "daily"
}
